"""These are the tiniest elements of the migrations.

An atom consists of both an up and a down Quark.

"""

from __future__ import with_statement

from contextlib import closing


__all__ = [ 'AdminConnection', 'AppConnection', 'BaseQuark', 'BooleanQuark', 'CountIsZero',
           'CreateDatabase', 'LogicalQuark', 'MigrationConnection', 'MigrationSucceeded', 'RawConnection',
           'SucceedIf', 'SucceedUnless', 'WithCleanConnection', 'MigrationWasApplied']

class BaseAtom(object):
    """Represents an up and a down atom"""
    
    def __init__(self, *args):
        (up, down) = self.get_up_and_down(args)
        self.up = self.wrapper(up)
        self.down = self.wrapper(down)

    def get_up_and_down(self, args):
        if len(args) == 2:
            return (args[0], args[1])
        if len(args) != 1:
            raise ValueError("Argument must be two quarks, a tuple, or an atom")
        else:
            arg = args[0]
        if isinstance(arg, tuple):
            return (arg[0], arg[1])
        elif isinstance(arg, BaseAtom):
            return (arg.up, arg.down)
        else:
            raise ValueError("Cannot determine molecule or atom type")
        
    def __str__(self):
        return '(%s, %s)' % (repr(self.up), repr(self.down))
    
    __repr__ = __str__
    

class Atom(BaseAtom):
    
    def wrapper(self, atom):
        return Quark(atom)


class CreateDatabase(BaseAtom):

    def __init__(self): # override from the base atom
        pass
    
    def up(self, datasrc, _conn):
        datasrc.create_database_if_needed()

    def down(self, datasrc, _conn):
        datasrc.drop_database_if_needed()

    def __str__(self):
        return self.__class__.__name__ + '()'

    __repr__ = __str__


class MigrationSucceeded(Exception):
    pass


def Quark(raw_quark):
    """Turns strings and callables into Quarks"""
    if isinstance(raw_quark, basestring):
        return SQLQuark(raw_quark)
    elif isinstance(raw_quark, BaseQuark):
        return raw_quark
    elif callable(raw_quark):
        return PythonQuark(raw_quark)
    else:
        raise ValueError("Unknown atom type")


class BaseQuark(object):
    
    def __init__(self, q):
        self.quark = Quark(q)
        
    def __call__(self, datasource, conn):
        raise Exception("Not implemented")
    
    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, str(self.quark))
    
    __repr__ = __str__


class SQLQuark(BaseQuark):

    class EmptySqlResults(object):

        @staticmethod
        def fetchall():
            return []

    def __init__(self, sql):
        self.sql = sql
        
    def __call__(self, datasource, conn):
        if not self.sql:
            return self.EmptySqlResults()
        return conn.execute(self.sql % datasource.expansions)
    
    def __str__(self):
        return repr(self.sql)

    __repr__ = __str__
    
    
class PythonQuark(BaseQuark):
    
    def __init__(self, func):
        self.func = func
        
    def __call__(self, datasource, conn):
        try:
            return self.func(datasrc=datasource, conn=conn.connection, exp=conn.expansions)
        except TypeError:
            try:
                return self.func(conn.connection, conn.expansions)
            except TypeError:
                return self.func(conn)
        
    def __str__(self):
        return self.func.__name__
    
    __repr__ = __str__


class RawConnection(BaseQuark):
    """This is a user level atom that prevents a transaction from occurring."""
    
    def __init__(self, sql):
        if not isinstance(sql, basestring):
            raise TypeError("raw connections must call sql statements")
        self.sql = sql
        
    def __call__(self, datasource, conn):
        with conn.raw_cursor() as c:
            c.execute(self.sql % datasource.expansions)
            
    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, repr(self.sql))

    __repr__ = __str__


class MigrationConnection(BaseQuark):
    """Runs on the migration connection"""

    def __call__(self, datasource, _conn):
        return self.quark(datasource, datasource.migration_conn)


class AdminConnection(BaseQuark):
    """Runs on the admin connection"""

    def __call__(self, datasource, _conn):
        return self.quark(datasource, datasource.admin_conn)


class AppConnection(BaseQuark):
    """Runs on the app connection"""

    def __call__(self, datasource, _conn):
        return self.quark(datasource, datasource.app_conn)


class WithCleanConnection(BaseQuark):
    """Closes all connections before invoking the enclosed quark"""
    
    def __call__(self, datasource, conn):
        datasource.close()
        return self.quark(datasource, conn)


class BooleanQuark(BaseQuark):
    """All quarks returning True/False must descend from this class"""
    pass


class LogicalQuark(BaseQuark):

    def __init__(self, boolean_quark):
        if not isinstance(boolean_quark, BooleanQuark):
            raise ValueError("can only be used with a BooleanQuark")
        self.quark = boolean_quark


class SucceedIf(LogicalQuark):
    """Succeeds if the enclosed quark is True"""
    
    def __call__(self, datasource, conn):
        if self.quark(datasource, conn):
            raise MigrationSucceeded()


class SucceedUnless(LogicalQuark):
    """Succeeds if the enclosed quark is False"""
    
    def __call__(self, datasource, conn):
        if not self.quark(datasource, conn):
            raise MigrationSucceeded()

        
class CountIsZero(BooleanQuark):
    
    def __call__(self, datasource, conn):
        rows = self.quark(datasource, conn).fetchall()
        return rows[0][0] == 0


class MigrationWasApplied(BooleanQuark):
    
    def __init__(self, id):
        if isinstance(id, int):
            id = str(id)
        if not isinstance(id, str):
            raise ValueError("is not string")
        self.id = id

    def __call__(self, datasource, conn):
        sql = "SELECT count(*) FROM %(schema_table)s WHERE %(schema_revision_col)s = '%(migration_id)s'"
        exp = datasource.expansions.copy() # don't modify the canonical copy
        exp['migration_id'] = self.id
        rows = conn.execute(sql % exp).fetchall()
        return rows[0][0] == 1
